Skip to content

feat(text): add MSDF font atlas text rendering module - #591

Open
stormmuller wants to merge 6 commits into
devfrom
claude/github-issue-584-erecu0
Open

feat(text): add MSDF font atlas text rendering module#591
stormmuller wants to merge 6 commits into
devfrom
claude/github-issue-584-erecu0

Conversation

@stormmuller

Copy link
Copy Markdown
Member

Summary

Adds a new @forge-game-engine/forge/text module implementing the MSDF (multi-channel signed distance field) text rendering path proposed in #584:

  • FontAtlas type + loadFontAtlas (/asset-loading) - parses an msdf-atlas-gen JSON metrics file and loads its atlas PNG (via the existing ImageCache) into glyph advances, kerning pairs, and line metrics, all normalized to em units. Kept data-only (no WebGL) so /asset-loading keeps its existing zero-dependency-on-rendering direction.
  • MSDF fragment shader + createMsdfTextRenderable (/rendering) - reuses the existing sprite.vert shader and its instance data layout (spriteInstanceDataSegment) completely unchanged, paired with a new msdf.frag shader (median-of-three-channels reconstruction + fwidth-based screen-space-derivative antialiasing, the standard technique from msdf-atlas-gen's own docs). One renderable is built per FontAtlas and shared across every entity using that font, exactly like a sprite's Renderable.
  • TextEcsComponent/addTextComponent, TextMeshEcsComponent, and createTextShapingEcsSystem (/text) - shapeText (pure, fully unit-tested) word-wraps, applies kerning, and lays out lines by alignment/line-spacing/pivot into glyph quads. The shaping system is dirty-tracked: it only re-shapes an entity when text, font, fontSize, wrapWidth, lineSpacing, alignment, or pivot actually changed, not every frame.
  • createRenderEcsSystem text integration - per the issue's own risk analysis, this skips the larger "generalize nine-slice into a shared SubQuad[] expansion" refactor in favor of the issue's suggested fallback: an isolated pushTextRenderCommands/buildTextCameraCommands pair that turns each glyph into a SpriteEcsComponent-shaped render command. This reuses the sprite instancing/batching/depth-sort pipeline completely unchanged (zero modifications to the existing sprite/nine-slice code path) while still letting text interleave correctly with sprites in one sorted command buffer.

Not included (follow-up work)

Flagged explicitly in the new docs' "Current limitations" section:

  • No font shipped with the engine - generating a real MSDF atlas requires the msdf-atlas-gen toolchain, which isn't available in this environment. Every project currently supplies its own atlas.
  • No Canvas2D prototyping escape hatch - the issue proposes a documented fillText-to-texture fallback for quick prototyping; not built here.
  • No text effects (outline/glow/shadow) - these fall out as extra MSDF shader parameters later.
  • No demo - without a real generated font atlas to point one at, a /documentation-site/src/pages/demos/text demo would have nothing genuine to render. Everything else in the module (shaping math, dirty tracking, shader, render-system integration) has full unit-test coverage instead.

Related issue(s)

Closes #584

Verification checklist

  • npm run check-types passes with 0 errors
  • npm test passes (1084 tests, 161 files)
  • npm run lint passes with 0 errors
  • npm run cspell passes with 0 errors
  • npm run check-exports passes (@forge-game-engine/forge/text resolves 🟢 ESM/bundler like every other subpath)
  • New public API is exported from each module's index.ts, /src/index.ts, and package.json exports (new ./text entry)
  • Documentation added under /documentation-site/docs/docs/text (overview + an MSDF Text guide covering atlas generation, wrapping/alignment, registration order, and dirty tracking), plus updates to the asset-loading and rendering section docs to cross-link it
  • No existing demo touches TextEcsComponent/the new msdf shader, so none needed updating - but since this PR does touch /rendering (render-system.ts, the shader cache, shader index), I still ran npm run build, documentation-site's npm run typecheck/npm run build, and loaded the Nine-Slice demo (the closest existing exerciser of the sprite render-command path this PR adds a sibling branch next to) plus the new /docs/text and /docs/text/msdf-text pages in a real browser (Playwright/Chromium) to confirm no regressions and no MDX/build errors

Changelog

  • Added a bullet under ## [Unreleased]#### Added in CHANGELOG.md

Generated by Claude Code

Adds `@forge-game-engine/forge/text`: `loadFontAtlas`/`FontAtlas`
(`/asset-loading`) parse an msdf-atlas-gen JSON metrics file and atlas PNG;
`createMsdfTextRenderable` (`/rendering`) builds the GPU-side renderable,
reusing the sprite vertex shader/instance data layout unchanged, paired
with a new MSDF fragment shader (median-of-three-channels + screen-space-
derivative antialiasing); `TextEcsComponent`/`addTextComponent` and
`createTextShapingEcsSystem` (`/text`) shape a string - word-wrapping,
kerning, alignment, line spacing - into glyph quads held on a
`TextMeshEcsComponent`, dirty-tracked so re-shaping only happens when a
shape-affecting field actually changes.

`createRenderEcsSystem` draws text through an isolated expansion branch
that turns each glyph into a `SpriteEcsComponent`-shaped render command,
so text batches and depth-sorts with sprites in the same command buffer
without touching the sprite/nine-slice hot path.

Closes #584. Ships the MSDF rendering path from the issue; a default
shipped font, a Canvas2D prototyping escape hatch, and text effects
(outline/glow/shadow) are follow-up work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gCpCEDFQad8Y9dJuahSq
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.40288% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/asset-loading/load-font-atlas.ts 90.00% 0 Missing and 2 partials ⚠️
src/rendering/systems/render-system.ts 91.30% 0 Missing and 2 partials ⚠️
src/text/utilities/shape-text.ts 98.30% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Per the add-demo skill: a new top-level /src module (text) unambiguously
needs a demo. Generates a real MSDF atlas from Liberation Sans (SIL OFL
1.1, bundled) via the actual Chlumsky msdf-atlas-gen toolchain (built from
source and vendored as static assets, see the README alongside them),
rather than skipping the demo for lack of a font asset.

The demo shows word-wrap/kerning/alignment side by side (left/center/
right, same paragraph) plus a live per-frame-updated counter label to
demonstrate createTextShapingEcsSystem's dirty tracking in action.

Also updates the text module's docs to link to the new demo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gCpCEDFQad8Y9dJuahSq

Copy link
Copy Markdown
Member Author

Added an interactive Text demo per the add-demo skill's own criteria (a new top-level /src module unambiguously needs one) — this was missing from the original PR.

Rather than skip it for lack of a font asset, built the actual msdf-atlas-gen C++ toolchain from source and generated a real MSDF atlas from Liberation Sans (SIL OFL 1.1, bundled under documentation-site/static/fonts/liberation-sans/ with its license and a regeneration recipe). This exercises loadFontAtlas against genuine msdf-atlas-gen JSON output for the first time (previously only unit-tested against hand-built fixtures), which is good added confidence in the parser.

The demo shows word-wrap/kerning/alignment side by side (left/center/right, same paragraph) plus a live per-frame-updated counter label demonstrating createTextShapingEcsSystem's dirty tracking. Verified in a real browser — screenshots confirmed crisp MSDF glyph rendering and the counter ticking frame to frame. documentation-site's typecheck and build both pass.


Generated by Claude Code

claude added 4 commits August 7, 2026 22:46
… Sans

Liberation Sans is SIL OFL 1.1, which raised concerns about how the
modification/redistribution terms read even though OFL is designed to be
embed-friendly. Open Sans (Apache License 2.0, full text bundled) has no
font-specific license terms at all - just the same permissive terms as
any other Apache-2.0 software - so it sidesteps the question entirely.

Regenerated the MSDF atlas from the real msdf-atlas-gen toolchain against
OpenSans-Regular.ttf and re-verified end-to-end (build, typecheck, and a
real browser load confirming crisp rendering and the live counter still
ticking).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gCpCEDFQad8Y9dJuahSq
The text docs were full of internal implementation detail a game author
doesn't need to know: GitHub issue references, "under the hood" render-
command internals, roadmap framing ("not yet implemented"), and a pointer
into the repo's own demo asset paths. Docs are for consumers of the
package, not people reading the engine's source.

Rewrites both pages around what a consumer actually needs: a complete,
runnable Quick Start, a full Options reference table for addTextComponent
(every field, type, default, and what it does), and a short factual list
of current constraints with no roadmap language. Trims the same kind of
internals aside out of the rendering module's overview page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gCpCEDFQad8Y9dJuahSq
…ivot fix

dev's #590 flipped SpriteEcsComponent.pivot to Y-up ((0, 0) bottom-left,
(1, 1) top-right) and fixed computeNineSliceRegions to match, but
shapeText has its own separate pivot math that was explicitly modeled on
nine-slice's old (Y-down) formula. That's now inconsistent with the rest
of the engine, including the sprite instancing pipeline text's own glyph
quads render through.

Applies the same fix nine-slice got: pivot.y's contribution flips via
(1 - pivot.y) instead of pivot.y directly. Only affects a non-centered
pivot; the default (0.5, 0.5) is unaffected. Updates the pivot tests and
docs to match.

Also merges dev in to pick up #590 and everything else merged since this
branch was created.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gCpCEDFQad8Y9dJuahSq
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(text): text rendering — MSDF font atlases, shaping, and a TextEcsComponent

2 participants